#pragma once

#include <algorithm>
#include <functional>
#include <iostream>
#include <list>
#include <mutex>
#include <optional>
#include <thread>
#include <tuple>
#include <type_traits>
#include <typeindex>

#include "./commands.hpp"
#include "./event.hpp"
#include "./stage.hpp"
#include "./utils.hpp"
#include "./world.hpp"

namespace qbe::ecs {
typedef struct AppExit {
} AppExit;

class Scheduler;

class Scheduler {

  using SystemId = size_t;

 private:
  std::list<std::type_index> _schedule;
  std::list<std::type_index> _at_start_schedule;
  SystemId last_id = 0;

 public:
  using system_function = std::function<void(World&)>;
  using system = std::tuple<system_function, std::type_index>;
  std::vector<system> _systems;
  std::unordered_map<std::type_index, std::vector<system_function>> _systems_map;
  std::unordered_map<std::string, std::type_index> _systems_name_map;

  Scheduler() : _stage(_at_start_schedule.begin()){};
  ~Scheduler() { _stop = false; };

  template <typename S, typename std::enable_if_t<std::is_same_v<typename S::is_repeat, std::true_type>, bool> = true>
  bool schedule_defined() {
    auto it = std::find(_schedule.begin(), _schedule.end(), std::type_index(typeid(S)));

    return (it != _schedule.end());
  }

  template <typename S, typename std::enable_if_t<std::is_same_v<typename S::is_repeat, std::false_type>, bool> = true>
  bool schedule_defined() {
    auto it = std::find(_at_start_schedule.begin(), _at_start_schedule.end(), std::type_index(typeid(S)));

    return (it != _at_start_schedule.end());
  }

  template <typename T, typename std::enable_if_t<std::is_same_v<typename T::is_repeat, std::true_type>, bool> = true>
  void insert_schedule() {
    if constexpr (!std::is_same_v<typename T::previous, std::nullopt_t>) {
      auto it = std::find(_schedule.begin(), _schedule.end(), std::type_index(typeid(typename T::previous)));

      _schedule.insert(it, std::type_index(typeid(T)));
    } else if constexpr (!std::is_same_v<typename T::next, std::nullopt_t>) {
      auto it = std::find(_schedule.begin(), _schedule.end(), std::type_index(typeid(typename T::next)));

      if (it != _schedule.end())
        ++it;
      _schedule.insert(it, std::type_index(typeid(T)));
    } else {
      _schedule.push_back(std::type_index(typeid(T)));
    }
  }

  template <typename T, typename std::enable_if_t<std::is_same_v<typename T::is_repeat, std::false_type>, bool> = true>
  void insert_schedule() {
    if constexpr (!std::is_same_v<typename T::previous, std::nullopt_t>) {
      auto it = std::find(_at_start_schedule.begin(), _at_start_schedule.end(), std::type_index(typeid(typename T::previous)));

      _at_start_schedule.insert(it, std::type_index(typeid(T)));
    } else if constexpr (!std::is_same_v<typename T::next, std::nullopt_t>) {
      auto it = std::find(_at_start_schedule.begin(), _at_start_schedule.end(), std::type_index(typeid(typename T::next)));

      auto t = std::type_index(typeid(T));
      auto t_next = std::type_index(typeid(typename T::next));

      if (it != _at_start_schedule.end())
        ++it;
      _at_start_schedule.insert(it, std::type_index(typeid(T)));
    } else {
      _at_start_schedule.push_back(std::type_index(typeid(T)));
    }
  }

  template <class F, class S, class... Args>
  void add_class_system(const F& func) {
    static_assert(all(Or<is_world<Args>, is_commands<Args>, is_query<Args>, is_bus<Args>, is_registry<Args>, is_resource<Args>, is_event_reader<Args>,
                         is_event_writer<Args>>()...),
                  "type must be reference to query, world, commands or resource");
    if (!schedule_defined<S>()) {
      std::cerr << "WARNING/Cevy: Stage not yet added to ecs pipeline" << std::endl;
    }

    system_function sys = [&func](World& reg) mutable {
      func(reg.get_super<Args>(0)...);
    };
    // _systems_name_map[typeid(S).name()] = std::type_index(typeid(S));
    _systems.push_back(std::make_tuple(sys, std::type_index(typeid(S))));
    _systems_map[std::type_index(typeid(S))].push_back(sys);
  }

  template <class R, class... Args>
  void add_system(R (&func)(Args...)) {
    add_system<stage::Update>(func);
  }

  template <class S, class R, class... Args>
  void add_system(const std::function<R(Args...)>& func) {
    static_assert(all(Or<is_world<Args>, is_commands<Args>, is_query<Args>, is_bus<Args>, is_registry<Args>, is_resource<Args>, is_event_reader<Args>,
                         is_event_writer<Args>>()...),
                  "type must be reference to query, world, commands or resource");
    if (!schedule_defined<S>()) {
      std::cerr << "WARNING/Cevy: Stage not yet added to ecs pipeline" << std::endl;
    }
    system_function sys = [&func](World& reg) mutable {
      func(reg.get_super<Args>(0)...);
    };
    // _systems_name_map[typeid(S).name()] = std::type_index(typeid(S));
    _systems.push_back(std::make_tuple(sys, std::type_index(typeid(S))));
    _systems_map[std::type_index(typeid(S))].push_back(sys);
  }

  template <class S, class R, class... Args>
  void add_system(R (&func)(Args...)) {
    static_assert(all(Or<is_world<Args>, is_commands<Args>, is_query<Args>, is_bus<Args>, is_registry<Args>, is_resource<Args>, is_event_reader<Args>,
                         is_event_writer<Args>>()...),
                  "type must be reference to query, world, commands or resource");
    // #ifdef DEBUG
    if (!schedule_defined<S>()) {
      std::cerr << "WARNING/Cevy: Stage not yet added to ecs pipeline" << std::endl;
    }
    // #endif
    system_function sys = [id = last_id, &func](World& reg) mutable {
      func(reg.get_super<Args>(id)...);
    };
    last_id += 1;
    // _systems_name_map[typeid(S).name()] = std::type_index(typeid(S));
    _systems.push_back(std::make_tuple(sys, std::type_index(typeid(S))));
    _systems_map[std::type_index(typeid(S))].push_back(sys);
  }

 protected:
  mutable bool _stop = false;
  std::list<std::type_index>::iterator _stage;
  std::shared_ptr<std::thread> _run_thread;

  void runStartStages(World& world);
  void runStages(World& world);
  void runStage(World& world);
  void go();

 public:
  void run(World& world);
  void run_setup(World& world);

 private:
  World* _world;
  std::mutex _mutex;
  int _cnt{0};

  uint32_t _fps_cnt{0};
  double _fps_time{0};
  std::clock_t _fps_pre_time{std::clock()};
  std::unordered_map<std::type_index, double> _systems_times;
};

}  // namespace qbe::ecs
